/* * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jkiss.utils.xml; import org.jkiss.utils.Base64; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Stream oriented XML document builder. */ public class XMLBuilder { public final class Element implements AutoCloseable { private Element parent; private String name; private Map<String, String> nsStack = null; private int level; Element( Element parent, String name) { this.init(parent, name); } void init( Element parent, String name) { this.parent = parent; this.name = name; this.nsStack = null; this.level = parent == null ? 0 : parent.level + 1; } public String getName() { return name; } public int getLevel() { return level; } public void addNamespace(String nsURI, String nsPrefix) { if (nsStack == null) { nsStack = new HashMap<>(); } nsStack.put(nsURI, nsPrefix); } public String getNamespacePrefix(String nsURI) { if (nsURI.equals(XMLConstants.NS_XML)) { return XMLConstants.PREFIX_XML; } String prefix = (nsStack == null ? null : nsStack.get(nsURI)); return prefix != null ? prefix : (parent != null ? parent.getNamespacePrefix(nsURI) : null); } @Override public void close() throws IOException { XMLBuilder.this.endElement(); } } // At the beginning and after tag closing private static final int STATE_NOTHING = 0; // After tag opening private static final int STATE_ELEM_OPENED = 1; // After text added private static final int STATE_TEXT_ADDED = 2; private static final int IO_BUFFER_SIZE = 8192; private java.io.Writer writer; private int state = STATE_NOTHING; private Element element = null; private boolean butify = false; private List<Element> trashElements = new java.util.ArrayList<>(); public XMLBuilder( java.io.OutputStream stream, String documentEncoding) throws java.io.IOException { this(stream, documentEncoding, true); } public XMLBuilder( java.io.OutputStream stream, String documentEncoding, boolean printHeader) throws java.io.IOException { if (documentEncoding == null) { this.init(new java.io.OutputStreamWriter(stream), null, printHeader); } else { this.init( new java.io.OutputStreamWriter(stream, documentEncoding), documentEncoding, printHeader); } } public XMLBuilder( java.io.Writer writer, String documentEncoding) throws java.io.IOException { this(writer, documentEncoding, true); } public XMLBuilder( java.io.Writer writer, String documentEncoding, boolean printHeader) throws java.io.IOException { this.init(writer, documentEncoding, printHeader); } private Element createElement( Element parent, String name) { if (trashElements.isEmpty()) { return new Element(parent, name); } else { Element element = trashElements.remove(trashElements.size() - 1); element.init(parent, name); return element; } } private void deleteElement( Element element) { trashElements.add(element); } private void init( java.io.Writer writer, String documentEncoding, boolean printHeader) throws java.io.IOException { this.writer = new java.io.BufferedWriter(writer, IO_BUFFER_SIZE); if (printHeader) { if (documentEncoding != null) { this.writer.write(XMLConstants.XML_HEADER(documentEncoding)); } else { this.writer.write(XMLConstants.XML_HEADER()); } } } public boolean isButify() { return butify; } public void setButify(boolean butify) { this.butify = butify; } public Element startElement( String elementName) throws java.io.IOException { return this.startElement(null, null, elementName); } public Element startElement( String nsURI, String elementName) throws java.io.IOException { return this.startElement(nsURI, null, elementName); } /* NS prefix will be used in element name if its directly specified as nsPrefix parameter or if nsURI has been declared above */ public Element startElement( String nsURI, String nsPrefix, String elementName) throws java.io.IOException { switch (state) { case STATE_ELEM_OPENED: writer.write('>'); case STATE_NOTHING: if (butify) { writer.write('\n'); } break; default: break; } if (butify) { if (element != null) { for (int i = 0; i <= element.getLevel(); i++) { writer.write('\t'); } } } writer.write('<'); boolean addNamespace = (nsURI != null); // If old nsURI specified - use prefix if (nsURI != null) { if (nsPrefix == null && element != null) { nsPrefix = element.getNamespacePrefix(nsURI); if (nsPrefix != null) { // Do not add NS declaration - it was declared somewhere above addNamespace = false; } } } // If we have prefix - use it in tag name if (nsPrefix != null) { elementName = nsPrefix + ':' + elementName; } writer.write(elementName); state = STATE_ELEM_OPENED; element = this.createElement(element, elementName); if (addNamespace) { this.addNamespace(nsURI, nsPrefix); element.addNamespace(nsURI, nsPrefix); } return element; } public XMLBuilder endElement() throws java.io.IOException, IllegalStateException { if (element == null) { throw new IllegalStateException("Close tag without open"); } switch (state) { case STATE_ELEM_OPENED: writer.write("/>"); break; case STATE_NOTHING: if (butify) { writer.write('\n'); for (int i = 0; i < element.getLevel(); i++) { writer.write('\t'); } } case STATE_TEXT_ADDED: writer.write("</"); writer.write(element.getName()); writer.write('>'); default: break; } this.deleteElement(element); element = element.parent; state = STATE_NOTHING; return this; } public XMLBuilder addNamespace(String nsURI) throws java.io.IOException { return this.addNamespace(nsURI, null); } public XMLBuilder addNamespace( String nsURI, String nsPrefix) throws java.io.IOException, IllegalStateException { if (element == null) { throw new IllegalStateException("Namespace outside of element"); } String attrName = XMLConstants.XMLNS; if (nsPrefix != null) { attrName = attrName + ':' + nsPrefix; element.addNamespace(nsURI, nsPrefix); } this.addAttribute(null, attrName, nsURI, true); return this; } public XMLBuilder addAttribute( String attributeName, String attributeValue) throws java.io.IOException { return this.addAttribute(null, attributeName, attributeValue, true); } public XMLBuilder addAttribute( String attributeName, int attributeValue) throws java.io.IOException { return this.addAttribute(null, attributeName, String.valueOf(attributeValue), false); } public XMLBuilder addAttribute( String attributeName, long attributeValue) throws java.io.IOException { return this.addAttribute(null, attributeName, String.valueOf(attributeValue), false); } public XMLBuilder addAttribute( String attributeName, boolean attributeValue) throws java.io.IOException { return this.addAttribute(null, attributeName, String.valueOf(attributeValue), false); } public XMLBuilder addAttribute( String attributeName, float attributeValue) throws java.io.IOException { return this.addAttribute(null, attributeName, String.valueOf(attributeValue), false); } public XMLBuilder addAttribute( String attributeName, double attributeValue) throws java.io.IOException { return this.addAttribute(null, attributeName, String.valueOf(attributeValue), false); } public XMLBuilder addAttribute( String nsURI, String attributeName, String attributeValue) throws java.io.IOException { return this.addAttribute(nsURI, attributeName, attributeValue, true); } private XMLBuilder addAttribute( String nsURI, String attributeName, String attributeValue, boolean escape) throws java.io.IOException, IllegalStateException { switch (state) { case STATE_ELEM_OPENED: { if (nsURI != null) { String nsPrefix = element.getNamespacePrefix(nsURI); if (nsPrefix == null) { throw new IllegalStateException( "Unknown attribute '" + attributeName + "' namespace URI '" + nsURI + "' in element '" + element.getName() + "'"); } attributeName = nsPrefix + ':' + attributeName; } writer.write(' '); writer.write(attributeName); writer.write("=\""); writer.write(escape ? XMLUtils.escapeXml(attributeValue) : attributeValue); writer.write('"'); break; } case STATE_TEXT_ADDED: case STATE_NOTHING: throw new IllegalStateException( "Attribute ouside of element"); default: break; } return this; } public XMLBuilder addText( CharSequence textValue) throws java.io.IOException { return addText(textValue, true); } public XMLBuilder addText( CharSequence textValue, boolean escape) throws java.io.IOException { switch (state) { case STATE_ELEM_OPENED: writer.write('>'); case STATE_TEXT_ADDED: case STATE_NOTHING: break; default: break; } this.writeText(textValue, escape); state = STATE_TEXT_ADDED; return this; } /** * Adds entire content of specified reader as text * * @param reader text reader * @return self reference * @throws java.io.IOException on IO error */ public XMLBuilder addText( java.io.Reader reader) throws java.io.IOException { switch (state) { case STATE_ELEM_OPENED: writer.write('>'); case STATE_TEXT_ADDED: case STATE_NOTHING: break; default: break; } writer.write("<![CDATA["); char[] writeBuffer = new char[8192]; for (int br = reader.read(writeBuffer); br != -1; br = reader.read(writeBuffer)) { writer.write(new String(writeBuffer, 0, br)); } writer.write("]]>"); state = STATE_TEXT_ADDED; return this; } public XMLBuilder addTextData( String text) throws java.io.IOException { switch (state) { case STATE_ELEM_OPENED: writer.write('>'); case STATE_TEXT_ADDED: case STATE_NOTHING: break; default: break; } writer.write("<![CDATA["); writer.write(text); writer.write("]]>"); state = STATE_TEXT_ADDED; return this; } /** * Adds content of specified stream as Base64 encoded text * * @param stream Input content stream * @param length Content length (this parameter must be correctly specified) * @return self reference * @throws java.io.IOException on IO error */ public XMLBuilder addBinary( java.io.InputStream stream, int length) throws java.io.IOException { switch (state) { case STATE_ELEM_OPENED: writer.write('>'); case STATE_TEXT_ADDED: case STATE_NOTHING: break; default: break; } Base64.encode(stream, length, writer); state = STATE_TEXT_ADDED; return this; } public XMLBuilder addBinary( byte[] buffer) throws java.io.IOException { switch (state) { case STATE_ELEM_OPENED: writer.write('>'); case STATE_TEXT_ADDED: case STATE_NOTHING: break; default: break; } Base64.encode(buffer, 0, buffer.length, writer); state = STATE_TEXT_ADDED; return this; } /** * Adds character content as is without any escaping or validation * @param textValue content * @return self reference * @throws java.io.IOException */ public XMLBuilder addContent( CharSequence textValue) throws java.io.IOException { writer.write(textValue.toString()); return this; } public XMLBuilder addComment( String commentValue) throws java.io.IOException { switch (state) { case STATE_ELEM_OPENED: writer.write('>'); case STATE_NOTHING: if (butify) { writer.write('\n'); } break; default: break; } writer.write("<!--"); writer.write(commentValue); writer.write("-->"); if (butify) { writer.write('\n'); } state = STATE_TEXT_ADDED; return this; } public XMLBuilder addElement( String elementName, String elementValue) throws java.io.IOException { this.startElement(elementName); this.addText(elementValue); this.endElement(); return this; } public XMLBuilder addElementText( String elementName, String elementValue) throws java.io.IOException { this.startElement(elementName); this.addTextData(elementValue); this.endElement(); return this; } public XMLBuilder flush() throws java.io.IOException { writer.flush(); return this; } private XMLBuilder writeText(CharSequence textValue, boolean escape) throws java.io.IOException { if (textValue != null) { writer.write(escape ? XMLUtils.escapeXml(textValue) : textValue.toString()); } return this; } }